/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Haystack Software Inc. All rights reserved.
 *  Licensed under the PolyForm Strict License 1.0.0. See License.txt in the project root for
 *  license information.
 *--------------------------------------------------------------------------------------------*/

/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See code-license.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import * as arrays from "vs/base/common/arrays"
import { IDisposable } from "vs/base/common/lifecycle"
import { WrappingIndent } from "vs/editor/common/config/editorOptions"
import { FontInfo } from "vs/editor/common/config/fontInfo"
import { IPosition, Position } from "vs/editor/common/core/position"
import { IRange, Range } from "vs/editor/common/core/range"
import {
  IModelDecoration,
  IModelDeltaDecoration,
  ITextModel,
  PositionAffinity,
} from "vs/editor/common/model"
import {
  IActiveIndentGuideInfo,
  BracketGuideOptions,
  IndentGuide,
  IndentGuideHorizontalLine,
} from "vs/editor/common/textModelGuides"
import { ModelDecorationOptions } from "vs/editor/common/model/textModel"
import { LineInjectedText } from "vs/editor/common/textModelEvents"
import * as viewEvents from "vs/editor/common/viewEvents"
import {
  createModelLineProjection,
  IModelLineProjection,
} from "vs/editor/common/viewModel/modelLineProjection"
import {
  ILineBreaksComputer,
  ModelLineProjectionData,
  InjectedText,
  ILineBreaksComputerFactory,
} from "vs/editor/common/modelLineProjectionData"
import { ConstantTimePrefixSumComputer } from "vs/editor/common/model/prefixSumComputer"
import { ICoordinatesConverter, ViewLineData } from "vs/editor/common/viewModel"

export interface IViewModelLines extends IDisposable {
  createCoordinatesConverter(): ICoordinatesConverter

  setWrappingSettings(
    fontInfo: FontInfo,
    wrappingStrategy: "simple" | "advanced",
    wrappingColumn: number,
    wrappingIndent: WrappingIndent,
    wordBreak: "normal" | "keepAll"
  ): boolean
  setTabSize(newTabSize: number): boolean
  getHiddenAreas(): Range[]
  setHiddenAreas(_ranges: readonly Range[]): boolean

  createLineBreaksComputer(): ILineBreaksComputer
  onModelFlushed(): void
  onModelLinesDeleted(
    versionId: number | null,
    fromLineNumber: number,
    toLineNumber: number
  ): viewEvents.ViewLinesDeletedEvent | null
  onModelLinesInserted(
    versionId: number | null,
    fromLineNumber: number,
    toLineNumber: number,
    lineBreaks: (ModelLineProjectionData | null)[]
  ): viewEvents.ViewLinesInsertedEvent | null
  onModelLineChanged(
    versionId: number | null,
    lineNumber: number,
    lineBreakData: ModelLineProjectionData | null
  ): [
    boolean,
    viewEvents.ViewLinesChangedEvent | null,
    viewEvents.ViewLinesInsertedEvent | null,
    viewEvents.ViewLinesDeletedEvent | null
  ]
  acceptVersionId(versionId: number): void

  getViewLineCount(): number
  getActiveIndentGuide(
    viewLineNumber: number,
    minLineNumber: number,
    maxLineNumber: number
  ): IActiveIndentGuideInfo
  getViewLinesIndentGuides(
    viewStartLineNumber: number,
    viewEndLineNumber: number
  ): number[]
  getViewLinesBracketGuides(
    startLineNumber: number,
    endLineNumber: number,
    activePosition: IPosition | null,
    options: BracketGuideOptions
  ): IndentGuide[][]
  getViewLineContent(viewLineNumber: number): string
  getViewLineLength(viewLineNumber: number): number
  getViewLineMinColumn(viewLineNumber: number): number
  getViewLineMaxColumn(viewLineNumber: number): number
  getViewLineData(viewLineNumber: number): ViewLineData
  getViewLinesData(
    viewStartLineNumber: number,
    viewEndLineNumber: number,
    needed: boolean[]
  ): Array<ViewLineData | null>

  getDecorationsInRange(
    range: Range,
    ownerId: number,
    filterOutValidation: boolean,
    onlyMinimapDecorations: boolean,
    onlyMarginDecorations: boolean
  ): IModelDecoration[]

  getInjectedTextAt(viewPosition: Position): InjectedText | null

  normalizePosition(position: Position, affinity: PositionAffinity): Position
  /**
   * Gets the column at which indentation stops at a given line.
   * @internal
   */
  getLineIndentColumn(lineNumber: number): number
}

export class ViewModelLinesFromProjectedModel implements IViewModelLines {
  private readonly _editorId: number
  private readonly model: ITextModel
  private _validModelVersionId: number

  private readonly _domLineBreaksComputerFactory: ILineBreaksComputerFactory
  private readonly _monospaceLineBreaksComputerFactory: ILineBreaksComputerFactory

  private fontInfo: FontInfo
  private tabSize: number
  private wrappingColumn: number
  private wrappingIndent: WrappingIndent
  private wordBreak: "normal" | "keepAll"
  private wrappingStrategy: "simple" | "advanced"

  private modelLineProjections!: IModelLineProjection[]

  /**
   * Reflects the sum of the line counts of all projected model lines.
   */
  private projectedModelLineLineCounts!: ConstantTimePrefixSumComputer

  private hiddenAreasDecorationIds!: string[]

  constructor(
    editorId: number,
    model: ITextModel,
    private readonly _editRange: IRange | undefined,
    domLineBreaksComputerFactory: ILineBreaksComputerFactory,
    monospaceLineBreaksComputerFactory: ILineBreaksComputerFactory,
    fontInfo: FontInfo,
    tabSize: number,
    wrappingStrategy: "simple" | "advanced",
    wrappingColumn: number,
    wrappingIndent: WrappingIndent,
    wordBreak: "normal" | "keepAll"
  ) {
    this._editorId = editorId
    this.model = model
    this._validModelVersionId = -1
    this._domLineBreaksComputerFactory = domLineBreaksComputerFactory
    this._monospaceLineBreaksComputerFactory =
      monospaceLineBreaksComputerFactory
    this.fontInfo = fontInfo
    this.tabSize = tabSize
    this.wrappingStrategy = wrappingStrategy
    this.wrappingColumn = wrappingColumn
    this.wrappingIndent = wrappingIndent
    this.wordBreak = wordBreak

    this._constructLines(/*resetHiddenAreas*/ true, null)
  }

  public dispose(): void {
    this.hiddenAreasDecorationIds = this.model.deltaDecorations(
      this.hiddenAreasDecorationIds,
      []
    )
  }

  public createCoordinatesConverter(): ICoordinatesConverter {
    return new CoordinatesConverter(this, this._editRange)
  }

  private _constructLines(
    resetHiddenAreas: boolean,
    previousLineBreaks: (ModelLineProjectionData | null)[] | null
  ): void {
    this.modelLineProjections = []

    if (resetHiddenAreas) {
      this.hiddenAreasDecorationIds = this.model.deltaDecorations(
        this.hiddenAreasDecorationIds,
        []
      )
    }

    const linesContent = this.model.getLinesContent()
    const finalLinesContent = this._editRange
      ? linesContent.slice(
          this._editRange.startLineNumber - 1,
          this._editRange.endLineNumber
        )
      : linesContent
    const injectedTextDecorations = this.model.getInjectedTextDecorations(
      this._editorId
    )
    const lineBreaksComputer = this.createLineBreaksComputer()
    const lineCount = finalLinesContent.length

    const injectedTextQueue = new arrays.ArrayQueue(
      LineInjectedText.fromDecorations(injectedTextDecorations)
    )
    for (let i = 0; i < lineCount; i++) {
      const lineInjectedText = injectedTextQueue.takeWhile(
        (t) => t.lineNumber === i + 1
      )
      lineBreaksComputer.addRequest(
        finalLinesContent[i],
        lineInjectedText,
        previousLineBreaks ? previousLineBreaks[i] : null
      )
    }
    const linesBreaks = lineBreaksComputer.finalize()

    const values: number[] = []

    const hiddenAreas = this.hiddenAreasDecorationIds
      .map((areaId) => this.model.getDecorationRange(areaId)!)
      .sort(Range.compareRangesUsingStarts)
    let hiddenAreaStart = 1,
      hiddenAreaEnd = 0
    let hiddenAreaIdx = -1
    let nextLineNumberToUpdateHiddenArea =
      hiddenAreaIdx + 1 < hiddenAreas.length ? hiddenAreaEnd + 1 : lineCount + 2

    for (let i = 0; i < lineCount; i++) {
      const lineNumber = i + 1

      if (lineNumber === nextLineNumberToUpdateHiddenArea) {
        hiddenAreaIdx++
        hiddenAreaStart = hiddenAreas[hiddenAreaIdx]!.startLineNumber
        hiddenAreaEnd = hiddenAreas[hiddenAreaIdx]!.endLineNumber
        nextLineNumberToUpdateHiddenArea =
          hiddenAreaIdx + 1 < hiddenAreas.length
            ? hiddenAreaEnd + 1
            : lineCount + 2
      }

      const isInHiddenArea =
        lineNumber >= hiddenAreaStart && lineNumber <= hiddenAreaEnd
      const line = createModelLineProjection(linesBreaks[i], !isInHiddenArea)
      values[i] = line.getViewLineCount()
      this.modelLineProjections[i] = line
    }

    this._validModelVersionId = this.model.getVersionId()

    this.projectedModelLineLineCounts = new ConstantTimePrefixSumComputer(
      values
    )
  }

  public getHiddenAreas(): Range[] {
    return this.hiddenAreasDecorationIds.map(
      (decId) => this.model.getDecorationRange(decId)!
    )
  }

  public setHiddenAreas(_ranges: Range[]): boolean {
    const validatedRanges = _ranges.map((r) => this.model.validateRange(r))
    const newRanges = normalizeLineRanges(validatedRanges)

    // TODO@Martin: Please stop calling this method on each model change!

    // This checks if there really was a change
    const oldRanges = this.hiddenAreasDecorationIds
      .map((areaId) => this.model.getDecorationRange(areaId)!)
      .sort(Range.compareRangesUsingStarts)
    if (newRanges.length === oldRanges.length) {
      let hasDifference = false
      for (let i = 0; i < newRanges.length; i++) {
        if (!newRanges[i].equalsRange(oldRanges[i])) {
          hasDifference = true
          break
        }
      }
      if (!hasDifference) {
        return false
      }
    }

    const newDecorations = newRanges.map<IModelDeltaDecoration>((r) => ({
      range: r,
      options: ModelDecorationOptions.EMPTY,
    }))

    this.hiddenAreasDecorationIds = this.model.deltaDecorations(
      this.hiddenAreasDecorationIds,
      newDecorations
    )

    const hiddenAreas = newRanges
    let hiddenAreaStart = 1,
      hiddenAreaEnd = 0
    let hiddenAreaIdx = -1
    let nextLineNumberToUpdateHiddenArea =
      hiddenAreaIdx + 1 < hiddenAreas.length
        ? hiddenAreaEnd + 1
        : this.modelLineProjections.length + 2

    let hasVisibleLine = false
    for (let i = 0; i < this.modelLineProjections.length; i++) {
      const lineNumber = i + 1

      if (lineNumber === nextLineNumberToUpdateHiddenArea) {
        hiddenAreaIdx++
        hiddenAreaStart = hiddenAreas[hiddenAreaIdx].startLineNumber
        hiddenAreaEnd = hiddenAreas[hiddenAreaIdx].endLineNumber
        nextLineNumberToUpdateHiddenArea =
          hiddenAreaIdx + 1 < hiddenAreas.length
            ? hiddenAreaEnd + 1
            : this.modelLineProjections.length + 2
      }

      let lineChanged = false
      if (lineNumber >= hiddenAreaStart && lineNumber <= hiddenAreaEnd) {
        // Line should be hidden
        if (this.modelLineProjections[i].isVisible()) {
          this.modelLineProjections[i] =
            this.modelLineProjections[i].setVisible(false)
          lineChanged = true
        }
      } else {
        hasVisibleLine = true
        // Line should be visible
        if (!this.modelLineProjections[i].isVisible()) {
          this.modelLineProjections[i] =
            this.modelLineProjections[i].setVisible(true)
          lineChanged = true
        }
      }
      if (lineChanged) {
        const newOutputLineCount =
          this.modelLineProjections[i].getViewLineCount()
        this.projectedModelLineLineCounts.setValue(i, newOutputLineCount)
      }
    }

    if (!hasVisibleLine) {
      // Cannot have everything be hidden => reveal everything!
      this.setHiddenAreas([])
    }

    return true
  }

  public modelPositionIsVisible(
    modelLineNumber: number,
    _modelColumn: number
  ): boolean {
    if (
      modelLineNumber < 1 ||
      modelLineNumber > this.modelLineProjections.length
    ) {
      // invalid arguments
      return false
    }
    return this.modelLineProjections[modelLineNumber - 1].isVisible()
  }

  public getModelLineViewLineCount(modelLineNumber: number): number {
    if (
      modelLineNumber < 1 ||
      modelLineNumber > this.modelLineProjections.length
    ) {
      // invalid arguments
      return 1
    }
    return this.modelLineProjections[modelLineNumber - 1].getViewLineCount()
  }

  public setTabSize(newTabSize: number): boolean {
    if (this.tabSize === newTabSize) {
      return false
    }
    this.tabSize = newTabSize

    this._constructLines(/*resetHiddenAreas*/ false, null)

    return true
  }

  public setWrappingSettings(
    fontInfo: FontInfo,
    wrappingStrategy: "simple" | "advanced",
    wrappingColumn: number,
    wrappingIndent: WrappingIndent,
    wordBreak: "normal" | "keepAll"
  ): boolean {
    const equalFontInfo = this.fontInfo.equals(fontInfo)
    const equalWrappingStrategy = this.wrappingStrategy === wrappingStrategy
    const equalWrappingColumn = this.wrappingColumn === wrappingColumn
    const equalWrappingIndent = this.wrappingIndent === wrappingIndent
    const equalWordBreak = this.wordBreak === wordBreak
    if (
      equalFontInfo &&
      equalWrappingStrategy &&
      equalWrappingColumn &&
      equalWrappingIndent &&
      equalWordBreak
    ) {
      return false
    }

    const onlyWrappingColumnChanged =
      equalFontInfo &&
      equalWrappingStrategy &&
      !equalWrappingColumn &&
      equalWrappingIndent &&
      equalWordBreak

    this.fontInfo = fontInfo
    this.wrappingStrategy = wrappingStrategy
    this.wrappingColumn = wrappingColumn
    this.wrappingIndent = wrappingIndent
    this.wordBreak = wordBreak

    let previousLineBreaks: (ModelLineProjectionData | null)[] | null = null
    if (onlyWrappingColumnChanged) {
      previousLineBreaks = []
      for (let i = 0, len = this.modelLineProjections.length; i < len; i++) {
        previousLineBreaks[i] = this.modelLineProjections[i].getProjectionData()
      }
    }

    this._constructLines(/*resetHiddenAreas*/ false, previousLineBreaks)

    return true
  }

  public createLineBreaksComputer(): ILineBreaksComputer {
    const lineBreaksComputerFactory =
      this.wrappingStrategy === "advanced"
        ? this._domLineBreaksComputerFactory
        : this._monospaceLineBreaksComputerFactory
    return lineBreaksComputerFactory.createLineBreaksComputer(
      this.fontInfo,
      this.tabSize,
      this.wrappingColumn,
      this.wrappingIndent,
      this.wordBreak
    )
  }

  public onModelFlushed(): void {
    this._constructLines(/*resetHiddenAreas*/ true, null)
  }

  public onModelLinesDeleted(
    versionId: number | null,
    fromLineNumber: number,
    toLineNumber: number
  ): viewEvents.ViewLinesDeletedEvent | null {
    if (!versionId || versionId <= this._validModelVersionId) {
      // Here we check for versionId in case the lines were reconstructed in the meantime.
      // We don't want to apply stale change events on top of a newer read model state.
      return null
    }

    fromLineNumber = Math.max(fromLineNumber, 1)
    toLineNumber = Math.min(toLineNumber, this.getViewLineCount())

    const outputFromLineNumber =
      fromLineNumber === 1
        ? 1
        : this.projectedModelLineLineCounts.getPrefixSum(fromLineNumber - 1) + 1
    const outputToLineNumber =
      this.projectedModelLineLineCounts.getPrefixSum(toLineNumber)

    this.modelLineProjections.splice(
      fromLineNumber - 1,
      toLineNumber - fromLineNumber + 1
    )
    this.projectedModelLineLineCounts.removeValues(
      fromLineNumber - 1,
      toLineNumber - fromLineNumber + 1
    )

    return new viewEvents.ViewLinesDeletedEvent(
      outputFromLineNumber,
      outputToLineNumber
    )
  }

  public onModelLinesInserted(
    versionId: number | null,
    fromLineNumber: number,
    _toLineNumber: number,
    lineBreaks: (ModelLineProjectionData | null)[]
  ): viewEvents.ViewLinesInsertedEvent | null {
    if (!versionId || versionId <= this._validModelVersionId) {
      // Here we check for versionId in case the lines were reconstructed in the meantime.
      // We don't want to apply stale change events on top of a newer read model state.
      return null
    }

    fromLineNumber = Math.max(fromLineNumber, 1)

    // cannot use this.getHiddenAreas() because those decorations have already seen the effect of this model change
    const isInHiddenArea =
      fromLineNumber > 2 &&
      !this.modelLineProjections[fromLineNumber - 2].isVisible()

    const outputFromLineNumber =
      fromLineNumber === 1
        ? 1
        : this.projectedModelLineLineCounts.getPrefixSum(fromLineNumber - 1) + 1

    let totalOutputLineCount = 0
    const insertLines: IModelLineProjection[] = []
    const insertPrefixSumValues: number[] = []

    for (let i = 0, len = lineBreaks.length; i < len; i++) {
      const line = createModelLineProjection(lineBreaks[i], !isInHiddenArea)
      insertLines.push(line)

      const outputLineCount = line.getViewLineCount()
      totalOutputLineCount += outputLineCount
      insertPrefixSumValues[i] = outputLineCount
    }

    // TODO@Alex: use arrays.arrayInsert
    this.modelLineProjections = this.modelLineProjections
      .slice(0, fromLineNumber - 1)
      .concat(insertLines)
      .concat(this.modelLineProjections.slice(fromLineNumber - 1))

    this.projectedModelLineLineCounts.insertValues(
      fromLineNumber - 1,
      insertPrefixSumValues
    )

    return new viewEvents.ViewLinesInsertedEvent(
      outputFromLineNumber,
      outputFromLineNumber + totalOutputLineCount - 1
    )
  }

  public onModelLineChanged(
    versionId: number | null,
    lineNumber: number,
    lineBreakData: ModelLineProjectionData | null
  ): [
    boolean,
    viewEvents.ViewLinesChangedEvent | null,
    viewEvents.ViewLinesInsertedEvent | null,
    viewEvents.ViewLinesDeletedEvent | null
  ] {
    if (versionId !== null && versionId <= this._validModelVersionId) {
      // Here we check for versionId in case the lines were reconstructed in the meantime.
      // We don't want to apply stale change events on top of a newer read model state.
      return [false, null, null, null]
    }

    const lineIndex = lineNumber - 1

    if (lineIndex < 0 || lineIndex >= this.modelLineProjections.length) {
      return [false, null, null, null]
    }

    const oldOutputLineCount =
      this.modelLineProjections[lineIndex].getViewLineCount()
    const isVisible = this.modelLineProjections[lineIndex].isVisible()
    const line = createModelLineProjection(lineBreakData, isVisible)
    this.modelLineProjections[lineIndex] = line
    const newOutputLineCount =
      this.modelLineProjections[lineIndex].getViewLineCount()

    let lineMappingChanged = false
    let changeFrom = 0
    let changeTo = -1
    let insertFrom = 0
    let insertTo = -1
    let deleteFrom = 0
    let deleteTo = -1

    if (oldOutputLineCount > newOutputLineCount) {
      changeFrom =
        this.projectedModelLineLineCounts.getPrefixSum(lineNumber - 1) + 1
      changeTo = changeFrom + newOutputLineCount - 1
      deleteFrom = changeTo + 1
      deleteTo = deleteFrom + (oldOutputLineCount - newOutputLineCount) - 1
      lineMappingChanged = true
    } else if (oldOutputLineCount < newOutputLineCount) {
      changeFrom =
        this.projectedModelLineLineCounts.getPrefixSum(lineNumber - 1) + 1
      changeTo = changeFrom + oldOutputLineCount - 1
      insertFrom = changeTo + 1
      insertTo = insertFrom + (newOutputLineCount - oldOutputLineCount) - 1
      lineMappingChanged = true
    } else {
      changeFrom =
        this.projectedModelLineLineCounts.getPrefixSum(lineNumber - 1) + 1
      changeTo = changeFrom + newOutputLineCount - 1
    }

    this.projectedModelLineLineCounts.setValue(lineIndex, newOutputLineCount)

    const viewLinesChangedEvent =
      changeFrom <= changeTo
        ? new viewEvents.ViewLinesChangedEvent(
            changeFrom,
            changeTo - changeFrom + 1
          )
        : null
    const viewLinesInsertedEvent =
      insertFrom <= insertTo
        ? new viewEvents.ViewLinesInsertedEvent(insertFrom, insertTo)
        : null
    const viewLinesDeletedEvent =
      deleteFrom <= deleteTo
        ? new viewEvents.ViewLinesDeletedEvent(deleteFrom, deleteTo)
        : null

    return [
      lineMappingChanged,
      viewLinesChangedEvent,
      viewLinesInsertedEvent,
      viewLinesDeletedEvent,
    ]
  }

  public acceptVersionId(versionId: number): void {
    this._validModelVersionId = versionId
    if (
      this.modelLineProjections.length === 1 &&
      !this.modelLineProjections[0].isVisible()
    ) {
      // At least one line must be visible => reset hidden areas
      this.setHiddenAreas([])
    }
  }

  public getViewLineCount(): number {
    return this.projectedModelLineLineCounts.getTotalSum()
  }

  private _toValidViewLineNumber(viewLineNumber: number): number {
    if (viewLineNumber < 1) {
      return 1
    }
    const viewLineCount = this.getViewLineCount()
    if (viewLineNumber > viewLineCount) {
      return viewLineCount
    }
    return viewLineNumber | 0
  }

  public getActiveIndentGuide(
    viewLineNumber: number,
    minLineNumber: number,
    maxLineNumber: number
  ): IActiveIndentGuideInfo {
    viewLineNumber = this._toValidViewLineNumber(viewLineNumber)
    minLineNumber = this._toValidViewLineNumber(minLineNumber)
    maxLineNumber = this._toValidViewLineNumber(maxLineNumber)

    const modelPosition = this.convertViewPositionToModelPosition(
      viewLineNumber,
      this.getViewLineMinColumn(viewLineNumber)
    )
    const modelMinPosition = this.convertViewPositionToModelPosition(
      minLineNumber,
      this.getViewLineMinColumn(minLineNumber)
    )
    const modelMaxPosition = this.convertViewPositionToModelPosition(
      maxLineNumber,
      this.getViewLineMinColumn(maxLineNumber)
    )
    const result = this.model.guides.getActiveIndentGuide(
      this.adjustModelLineNumber(modelPosition.lineNumber),
      this.adjustModelLineNumber(modelMinPosition.lineNumber),
      this.adjustModelLineNumber(modelMaxPosition.lineNumber)
    )

    const viewStartPosition = this.convertModelPositionToViewPosition(
      this.adjustViewLineNumber(result.startLineNumber),
      1
    )
    const viewEndPosition = this.convertModelPositionToViewPosition(
      this.adjustViewLineNumber(result.endLineNumber),
      this.model.getLineMaxColumn(result.endLineNumber)
    )
    return {
      startLineNumber: viewStartPosition.lineNumber,
      endLineNumber: viewEndPosition.lineNumber,
      indent: result.indent,
    }
  }

  private adjustModelLineNumber(lineNumber: number): number {
    return (
      lineNumber + (this._editRange ? this._editRange.startLineNumber - 1 : 0)
    )
  }

  private adjustViewLineNumber(lineNumber: number): number {
    return (
      lineNumber - (this._editRange ? this._editRange.startLineNumber + 1 : 0)
    )
  }

  // #region ViewLineInfo

  private getViewLineInfo(viewLineNumber: number): ViewLineInfo {
    viewLineNumber = this._toValidViewLineNumber(viewLineNumber)
    const r = this.projectedModelLineLineCounts.getIndexOf(viewLineNumber - 1)
    const lineIndex = r.index
    const remainder = r.remainder
    return new ViewLineInfo(lineIndex + 1, remainder)
  }

  private getMinColumnOfViewLine(viewLineInfo: ViewLineInfo): number {
    return this.modelLineProjections[
      viewLineInfo.modelLineNumber - 1
    ].getViewLineMinColumn(
      this.model,
      viewLineInfo.modelLineNumber,
      viewLineInfo.modelLineWrappedLineIdx
    )
  }

  private getMaxColumnOfViewLine(viewLineInfo: ViewLineInfo): number {
    return this.modelLineProjections[
      viewLineInfo.modelLineNumber - 1
    ].getViewLineMaxColumn(
      this.model,
      viewLineInfo.modelLineNumber,
      viewLineInfo.modelLineWrappedLineIdx
    )
  }

  private getModelStartPositionOfViewLine(
    viewLineInfo: ViewLineInfo
  ): Position {
    const line = this.modelLineProjections[viewLineInfo.modelLineNumber - 1]
    const minViewColumn = line.getViewLineMinColumn(
      this.model,
      viewLineInfo.modelLineNumber,
      viewLineInfo.modelLineWrappedLineIdx
    )
    const column = line.getModelColumnOfViewPosition(
      viewLineInfo.modelLineWrappedLineIdx,
      minViewColumn
    )
    return new Position(viewLineInfo.modelLineNumber, column)
  }

  private getModelEndPositionOfViewLine(viewLineInfo: ViewLineInfo): Position {
    const line = this.modelLineProjections[viewLineInfo.modelLineNumber - 1]
    const maxViewColumn = line.getViewLineMaxColumn(
      this.model,
      viewLineInfo.modelLineNumber,
      viewLineInfo.modelLineWrappedLineIdx
    )
    const column = line.getModelColumnOfViewPosition(
      viewLineInfo.modelLineWrappedLineIdx,
      maxViewColumn
    )
    return new Position(viewLineInfo.modelLineNumber, column)
  }

  private getViewLineInfosGroupedByModelRanges(
    viewStartLineNumber: number,
    viewEndLineNumber: number
  ): ViewLineInfoGroupedByModelRange[] {
    const startViewLine = this.getViewLineInfo(viewStartLineNumber)
    const endViewLine = this.getViewLineInfo(viewEndLineNumber)

    const result = new Array<ViewLineInfoGroupedByModelRange>()
    let lastVisibleModelPos: Position | null =
      this.getModelStartPositionOfViewLine(startViewLine)
    let viewLines = new Array<ViewLineInfo>()

    for (
      let curModelLine = startViewLine.modelLineNumber;
      curModelLine <= endViewLine.modelLineNumber;
      curModelLine++
    ) {
      const line = this.modelLineProjections[curModelLine - 1]

      if (line.isVisible()) {
        const startOffset =
          curModelLine === startViewLine.modelLineNumber
            ? startViewLine.modelLineWrappedLineIdx
            : 0

        const endOffset =
          curModelLine === endViewLine.modelLineNumber
            ? endViewLine.modelLineWrappedLineIdx + 1
            : line.getViewLineCount()

        for (let i = startOffset; i < endOffset; i++) {
          viewLines.push(new ViewLineInfo(curModelLine, i))
        }
      }

      if (!line.isVisible() && lastVisibleModelPos) {
        const lastVisibleModelPos2 = new Position(
          curModelLine - 1,
          this.model.getLineMaxColumn(curModelLine - 1) + 1
        )

        const modelRange = Range.fromPositions(
          lastVisibleModelPos,
          lastVisibleModelPos2
        )
        result.push(new ViewLineInfoGroupedByModelRange(modelRange, viewLines))
        viewLines = []

        lastVisibleModelPos = null
      } else if (line.isVisible() && !lastVisibleModelPos) {
        lastVisibleModelPos = new Position(curModelLine, 1)
      }
    }

    if (lastVisibleModelPos) {
      const modelRange = Range.fromPositions(
        lastVisibleModelPos,
        this.getModelEndPositionOfViewLine(endViewLine)
      )
      result.push(new ViewLineInfoGroupedByModelRange(modelRange, viewLines))
    }

    return result
  }

  // #endregion

  public getViewLinesBracketGuides(
    viewStartLineNumber: number,
    viewEndLineNumber: number,
    activeViewPosition: IPosition | null,
    options: BracketGuideOptions
  ): IndentGuide[][] {
    const modelActivePosition = activeViewPosition
      ? this.convertViewPositionToModelPosition(
          activeViewPosition.lineNumber,
          activeViewPosition.column
        )
      : null
    const resultPerViewLine: IndentGuide[][] = []

    for (const group of this.getViewLineInfosGroupedByModelRanges(
      viewStartLineNumber,
      viewEndLineNumber
    )) {
      const modelRangeStartLineNumber = group.modelRange.startLineNumber

      const bracketGuidesPerModelLine = this.model.guides.getLinesBracketGuides(
        modelRangeStartLineNumber,
        group.modelRange.endLineNumber,
        modelActivePosition,
        options
      )

      for (const viewLineInfo of group.viewLines) {
        const bracketGuides =
          bracketGuidesPerModelLine[
            viewLineInfo.modelLineNumber - modelRangeStartLineNumber
          ]

        // visibleColumns stay as they are (this is a bug and needs to be fixed, but it is not a regression)
        // model-columns must be converted to view-model columns.
        const result = bracketGuides.map((g) => {
          if (g.forWrappedLinesAfterColumn !== -1) {
            const p = this.modelLineProjections[
              viewLineInfo.modelLineNumber - 1
            ].getViewPositionOfModelPosition(0, g.forWrappedLinesAfterColumn)
            if (p.lineNumber >= viewLineInfo.modelLineWrappedLineIdx) {
              return undefined
            }
          }

          if (g.forWrappedLinesBeforeOrAtColumn !== -1) {
            const p = this.modelLineProjections[
              viewLineInfo.modelLineNumber - 1
            ].getViewPositionOfModelPosition(
              0,
              g.forWrappedLinesBeforeOrAtColumn
            )
            if (p.lineNumber < viewLineInfo.modelLineWrappedLineIdx) {
              return undefined
            }
          }

          if (!g.horizontalLine) {
            return g
          }

          let column = -1
          if (g.column !== -1) {
            const p = this.modelLineProjections[
              viewLineInfo.modelLineNumber - 1
            ].getViewPositionOfModelPosition(0, g.column)
            if (p.lineNumber === viewLineInfo.modelLineWrappedLineIdx) {
              column = p.column
            } else if (p.lineNumber < viewLineInfo.modelLineWrappedLineIdx) {
              column = this.getMinColumnOfViewLine(viewLineInfo)
            } else if (p.lineNumber > viewLineInfo.modelLineWrappedLineIdx) {
              return undefined
            }
          }

          const viewPosition = this.convertModelPositionToViewPosition(
            viewLineInfo.modelLineNumber,
            g.horizontalLine.endColumn
          )
          const p = this.modelLineProjections[
            viewLineInfo.modelLineNumber - 1
          ].getViewPositionOfModelPosition(0, g.horizontalLine.endColumn)
          if (p.lineNumber === viewLineInfo.modelLineWrappedLineIdx) {
            return new IndentGuide(
              g.visibleColumn,
              column,
              g.className,
              new IndentGuideHorizontalLine(
                g.horizontalLine.top,
                viewPosition.column
              ),
              -1,
              -1
            )
          } else if (p.lineNumber < viewLineInfo.modelLineWrappedLineIdx) {
            return undefined
          } else {
            if (g.visibleColumn !== -1) {
              // Don't repeat horizontal lines that use visibleColumn for unrelated lines.
              return undefined
            }
            return new IndentGuide(
              g.visibleColumn,
              column,
              g.className,
              new IndentGuideHorizontalLine(
                g.horizontalLine.top,
                this.getMaxColumnOfViewLine(viewLineInfo)
              ),
              -1,
              -1
            )
          }
        })
        resultPerViewLine.push(result.filter((r): r is IndentGuide => !!r))
      }
    }

    return resultPerViewLine
  }

  public getViewLinesIndentGuides(
    viewStartLineNumber: number,
    viewEndLineNumber: number
  ): number[] {
    // TODO: Use the same code as in `getViewLinesBracketGuides`.
    // Future TODO: Merge with `getViewLinesBracketGuides`.
    // However, this requires more refactoring of indent guides.
    viewStartLineNumber = this._toValidViewLineNumber(viewStartLineNumber)
    viewEndLineNumber = this._toValidViewLineNumber(viewEndLineNumber)

    const modelStart = this.convertViewPositionToModelPosition(
      viewStartLineNumber,
      this.getViewLineMinColumn(viewStartLineNumber)
    )
    const modelEnd = this.convertViewPositionToModelPosition(
      viewEndLineNumber,
      this.getViewLineMaxColumn(viewEndLineNumber)
    )

    let result: number[] = []
    const resultRepeatCount: number[] = []
    const resultRepeatOption: IndentGuideRepeatOption[] = []
    const modelStartLineIndex = modelStart.lineNumber - 1
    const modelEndLineIndex = modelEnd.lineNumber - 1

    let reqStart: Position | null = null
    for (
      let modelLineIndex = modelStartLineIndex;
      modelLineIndex <= modelEndLineIndex;
      modelLineIndex++
    ) {
      const line = this.modelLineProjections[modelLineIndex]
      const adjustedModelLineIndex = this.adjustModelLineNumber(modelLineIndex)
      if (line.isVisible()) {
        const viewLineStartIndex = line.getViewLineNumberOfModelPosition(
          0,
          modelLineIndex === modelStartLineIndex ? modelStart.column : 1
        )
        const viewLineEndIndex = line.getViewLineNumberOfModelPosition(
          0,
          this.model.getLineMaxColumn(modelLineIndex + 1)
        )
        const count = viewLineEndIndex - viewLineStartIndex + 1
        let option = IndentGuideRepeatOption.BlockNone
        if (
          count > 1 &&
          line.getViewLineMinColumn(
            this.model,
            adjustedModelLineIndex + 1,
            viewLineEndIndex
          ) === 1
        ) {
          // wrapped lines should block indent guides
          option =
            viewLineStartIndex === 0
              ? IndentGuideRepeatOption.BlockSubsequent
              : IndentGuideRepeatOption.BlockAll
        }
        resultRepeatCount.push(count)
        resultRepeatOption.push(option)
        // merge into previous request
        if (reqStart === null) {
          reqStart = new Position(adjustedModelLineIndex + 1, 0)
        }
      } else {
        // hit invisible line => flush request
        if (reqStart !== null) {
          result = result.concat(
            this.model.guides.getLinesIndentGuides(
              reqStart.lineNumber,
              adjustedModelLineIndex
            )
          )
          reqStart = null
        }
      }
    }

    const adjustedModelEndLineIndex = this.adjustModelLineNumber(
      modelEnd.lineNumber
    )
    if (reqStart !== null) {
      result = result.concat(
        this.model.guides.getLinesIndentGuides(
          reqStart.lineNumber,
          adjustedModelEndLineIndex
        )
      )
      reqStart = null
    }

    const viewLineCount = viewEndLineNumber - viewStartLineNumber + 1
    const viewIndents = new Array<number>(viewLineCount)
    let currIndex = 0
    for (let i = 0, len = result.length; i < len; i++) {
      let value = result[i]
      const count = Math.min(viewLineCount - currIndex, resultRepeatCount[i])
      const option = resultRepeatOption[i]
      let blockAtIndex: number
      if (option === IndentGuideRepeatOption.BlockAll) {
        blockAtIndex = 0
      } else if (option === IndentGuideRepeatOption.BlockSubsequent) {
        blockAtIndex = 1
      } else {
        blockAtIndex = count
      }
      for (let j = 0; j < count; j++) {
        if (j === blockAtIndex) {
          value = 0
        }
        viewIndents[currIndex++] = value
      }
    }
    return viewIndents
  }

  public getViewLineContent(viewLineNumber: number): string {
    const info = this.getViewLineInfo(viewLineNumber)
    const finalLineNumber = this.adjustModelLineNumber(info.modelLineNumber)
    return this.modelLineProjections[
      info.modelLineNumber - 1
    ].getViewLineContent(
      this.model,
      finalLineNumber,
      info.modelLineWrappedLineIdx
    )
  }

  public getViewLineLength(viewLineNumber: number): number {
    const info = this.getViewLineInfo(viewLineNumber)
    const finalLineNumber = this.adjustModelLineNumber(info.modelLineNumber)
    return this.modelLineProjections[
      info.modelLineNumber - 1
    ].getViewLineLength(
      this.model,
      finalLineNumber,
      info.modelLineWrappedLineIdx
    )
  }

  public getViewLineMinColumn(viewLineNumber: number): number {
    const info = this.getViewLineInfo(viewLineNumber)
    const finalLineNumber = this.adjustModelLineNumber(info.modelLineNumber)
    return this.modelLineProjections[
      info.modelLineNumber - 1
    ].getViewLineMinColumn(
      this.model,
      finalLineNumber,
      info.modelLineWrappedLineIdx
    )
  }

  public getViewLineMaxColumn(viewLineNumber: number): number {
    const info = this.getViewLineInfo(viewLineNumber)
    const finalLineNumber = this.adjustModelLineNumber(info.modelLineNumber)
    return this.modelLineProjections[
      info.modelLineNumber - 1
    ].getViewLineMaxColumn(
      this.model,
      finalLineNumber,
      info.modelLineWrappedLineIdx
    )
  }

  public getViewLineData(viewLineNumber: number): ViewLineData {
    const info = this.getViewLineInfo(viewLineNumber)
    const adjustedModelLineNumber = this.adjustModelLineNumber(
      info.modelLineNumber
    )
    return this.modelLineProjections[info.modelLineNumber - 1].getViewLineData(
      this.model,
      adjustedModelLineNumber,
      info.modelLineWrappedLineIdx
    )
  }

  public getViewLinesData(
    viewStartLineNumber: number,
    viewEndLineNumber: number,
    needed: boolean[]
  ): ViewLineData[] {
    viewStartLineNumber = this._toValidViewLineNumber(viewStartLineNumber)
    viewEndLineNumber = this._toValidViewLineNumber(viewEndLineNumber)

    const start = this.projectedModelLineLineCounts.getIndexOf(
      viewStartLineNumber - 1
    )
    let viewLineNumber = viewStartLineNumber
    const startModelLineIndex = start.index
    const startRemainder = start.remainder

    const result: ViewLineData[] = []
    for (
      let modelLineIndex = startModelLineIndex,
        len = this._editRange
          ? this._editRange.endLineNumber
          : this.model.getLineCount();
      modelLineIndex < len;
      modelLineIndex++
    ) {
      const line = this.modelLineProjections[modelLineIndex]
      if (!line.isVisible()) {
        continue
      }
      const fromViewLineIndex =
        modelLineIndex === startModelLineIndex ? startRemainder : 0
      let remainingViewLineCount = line.getViewLineCount() - fromViewLineIndex

      let lastLine = false
      if (viewLineNumber + remainingViewLineCount > viewEndLineNumber) {
        lastLine = true
        remainingViewLineCount = viewEndLineNumber - viewLineNumber + 1
      }

      const finalLineNumber = this.adjustModelLineNumber(modelLineIndex)
      line.getViewLinesData(
        this.model,
        finalLineNumber + 1,
        fromViewLineIndex,
        remainingViewLineCount,
        viewLineNumber - viewStartLineNumber,
        needed,
        result
      )

      viewLineNumber += remainingViewLineCount

      if (lastLine) {
        break
      }
    }

    return result
  }

  public validateViewPosition(
    viewLineNumber: number,
    viewColumn: number,
    expectedModelPosition: Position
  ): Position {
    viewLineNumber = this._toValidViewLineNumber(viewLineNumber)

    const r = this.projectedModelLineLineCounts.getIndexOf(viewLineNumber - 1)
    const lineIndex = r.index
    const remainder = r.remainder

    const line = this.modelLineProjections[lineIndex]

    const adjustedLineIndex = this.adjustModelLineNumber(lineIndex + 1)
    const minColumn = line.getViewLineMinColumn(
      this.model,
      adjustedLineIndex,
      remainder
    )
    const maxColumn = line.getViewLineMaxColumn(
      this.model,
      adjustedLineIndex,
      remainder
    )
    if (viewColumn < minColumn) {
      viewColumn = minColumn
    }
    if (viewColumn > maxColumn) {
      viewColumn = maxColumn
    }

    const computedModelColumn = line.getModelColumnOfViewPosition(
      remainder,
      viewColumn
    )
    const computedModelPosition = this.validatePosition(
      new Position(lineIndex + 1, computedModelColumn)
    )

    if (computedModelPosition.equals(expectedModelPosition)) {
      return new Position(viewLineNumber, viewColumn)
    }

    return this.convertModelPositionToViewPosition(
      computedModelPosition.lineNumber,
      computedModelPosition.column
    )
  }

  public validateViewRange(viewRange: Range, expectedModelRange: Range): Range {
    const validViewStart = this.validateViewPosition(
      viewRange.startLineNumber,
      viewRange.startColumn,
      expectedModelRange.getStartPosition()
    )
    const validViewEnd = this.validateViewPosition(
      viewRange.endLineNumber,
      viewRange.endColumn,
      expectedModelRange.getEndPosition()
    )
    return new Range(
      validViewStart.lineNumber,
      validViewStart.column,
      validViewEnd.lineNumber,
      validViewEnd.column
    )
  }

  public convertViewPositionToModelPosition(
    viewLineNumber: number,
    viewColumn: number
  ): Position {
    const info = this.getViewLineInfo(viewLineNumber)

    const inputColumn = this.modelLineProjections[
      info.modelLineNumber - 1
    ].getModelColumnOfViewPosition(info.modelLineWrappedLineIdx, viewColumn)
    // console.log('out -> in ' + viewLineNumber + ',' + viewColumn + ' ===> ' + (lineIndex+1) + ',' + inputColumn);
    return this.validatePosition(
      new Position(info.modelLineNumber, inputColumn)
    )
  }

  public convertViewRangeToModelRange(viewRange: Range): Range {
    const start = this.convertViewPositionToModelPosition(
      viewRange.startLineNumber,
      viewRange.startColumn
    )
    const end = this.convertViewPositionToModelPosition(
      viewRange.endLineNumber,
      viewRange.endColumn
    )
    return new Range(start.lineNumber, start.column, end.lineNumber, end.column)
  }

  public convertModelPositionToViewPosition(
    _modelLineNumber: number,
    _modelColumn: number,
    affinity: PositionAffinity = PositionAffinity.None,
    allowZeroLineNumber: boolean = false,
    belowHiddenRanges: boolean = false
  ): Position {
    const validPosition = this.validatePosition(
      new Position(_modelLineNumber, _modelColumn)
    )
    const inputLineNumber = validPosition.lineNumber
    const inputColumn = validPosition.column

    let lineIndex = inputLineNumber - 1,
      lineIndexChanged = false
    if (belowHiddenRanges) {
      while (
        lineIndex < this.modelLineProjections.length &&
        !this.modelLineProjections[lineIndex].isVisible()
      ) {
        lineIndex++
        lineIndexChanged = true
      }
    } else {
      while (
        lineIndex > 0 &&
        !this.modelLineProjections[lineIndex].isVisible()
      ) {
        lineIndex--
        lineIndexChanged = true
      }
    }
    if (lineIndex === 0 && !this.modelLineProjections[lineIndex].isVisible()) {
      // Could not reach a real line
      // console.log('in -> out ' + inputLineNumber + ',' + inputColumn + ' ===> ' + 1 + ',' + 1);
      // TODO@alexdima@hediet this isn't soo pretty
      return new Position(allowZeroLineNumber ? 0 : 1, 1)
    }
    const deltaLineNumber =
      1 + this.projectedModelLineLineCounts.getPrefixSum(lineIndex)

    let r: Position
    if (lineIndexChanged) {
      if (belowHiddenRanges) {
        r = this.modelLineProjections[lineIndex].getViewPositionOfModelPosition(
          deltaLineNumber,
          1,
          affinity
        )
      } else {
        const adjustedLineIndex = this.adjustModelLineNumber(lineIndex + 1)
        r = this.modelLineProjections[lineIndex].getViewPositionOfModelPosition(
          deltaLineNumber,
          this.model.getLineMaxColumn(adjustedLineIndex),
          affinity
        )
      }
    } else {
      r = this.modelLineProjections[
        inputLineNumber - 1
      ].getViewPositionOfModelPosition(deltaLineNumber, inputColumn, affinity)
    }

    // console.log('in -> out ' + inputLineNumber + ',' + inputColumn + ' ===> ' + r.lineNumber + ',' + r);
    return r
  }

  private validatePosition(position: IPosition): Position {
    if (this._editRange == null) {
      return this.model.validatePosition(position)
    }
    const lineNumber = Math.max(
      1,
      Math.min(
        position.lineNumber,
        this._editRange.endLineNumber - this._editRange.startLineNumber + 1
      )
    )
    return new Position(
      lineNumber,
      Math.min(
        position.column,
        this.model.getLineMaxColumn(this.adjustModelLineNumber(lineNumber))
      )
    )
  }

  /**
   * @param affinity The affinity in case of an empty range. Has no effect for non-empty ranges.
   */
  public convertModelRangeToViewRange(
    modelRange: Range,
    affinity: PositionAffinity = PositionAffinity.Left
  ): Range {
    if (modelRange.isEmpty()) {
      const start = this.convertModelPositionToViewPosition(
        modelRange.startLineNumber,
        modelRange.startColumn,
        affinity
      )
      return Range.fromPositions(start)
    } else {
      const start = this.convertModelPositionToViewPosition(
        modelRange.startLineNumber,
        modelRange.startColumn,
        PositionAffinity.Right
      )
      const end = this.convertModelPositionToViewPosition(
        modelRange.endLineNumber,
        modelRange.endColumn,
        PositionAffinity.Left
      )
      return new Range(
        start.lineNumber,
        start.column,
        end.lineNumber,
        end.column
      )
    }
  }

  public getViewLineNumberOfModelPosition(
    modelLineNumber: number,
    modelColumn: number
  ): number {
    let lineIndex = modelLineNumber - 1

    if (this.modelLineProjections[lineIndex].isVisible()) {
      // this model line is visible
      const deltaLineNumber =
        1 + this.projectedModelLineLineCounts.getPrefixSum(lineIndex)
      return this.modelLineProjections[
        lineIndex
      ].getViewLineNumberOfModelPosition(deltaLineNumber, modelColumn)
    }

    // this model line is not visible
    while (lineIndex > 0 && !this.modelLineProjections[lineIndex].isVisible()) {
      lineIndex--
    }
    if (lineIndex === 0 && !this.modelLineProjections[lineIndex].isVisible()) {
      // Could not reach a real line
      return 1
    }
    const deltaLineNumber =
      1 + this.projectedModelLineLineCounts.getPrefixSum(lineIndex)
    return this.modelLineProjections[
      lineIndex
    ].getViewLineNumberOfModelPosition(
      deltaLineNumber,
      this.model.getLineMaxColumn(lineIndex + 1)
    )
  }

  public getDecorationsInRange(
    range: Range,
    ownerId: number,
    filterOutValidation: boolean,
    onlyMinimapDecorations: boolean,
    onlyMarginDecorations: boolean
  ): IModelDecoration[] {
    const modelStart = this.convertViewPositionToModelPosition(
      range.startLineNumber,
      range.startColumn
    )
    const modelEnd = this.convertViewPositionToModelPosition(
      range.endLineNumber,
      range.endColumn
    )

    if (
      modelEnd.lineNumber - modelStart.lineNumber <=
      range.endLineNumber - range.startLineNumber
    ) {
      // most likely there are no hidden lines => fast path
      // fetch decorations from column 1 to cover the case of wrapped lines that have whole line decorations at column 1
      const adjustedModelStartLineNumber = this.adjustModelLineNumber(
        modelStart.lineNumber
      )
      const adjustedModelEndLineNumber = this.adjustModelLineNumber(
        modelEnd.lineNumber
      )
      return this.model.getDecorationsInRange(
        new Range(
          adjustedModelStartLineNumber,
          1,
          adjustedModelEndLineNumber,
          modelEnd.column
        ),
        ownerId,
        filterOutValidation,
        onlyMinimapDecorations,
        onlyMarginDecorations
      )
    }

    let result: IModelDecoration[] = []
    const modelStartLineIndex = modelStart.lineNumber - 1
    const modelEndLineIndex = modelEnd.lineNumber - 1

    let reqStart: Position | null = null
    for (
      let modelLineIndex = modelStartLineIndex;
      modelLineIndex <= modelEndLineIndex;
      modelLineIndex++
    ) {
      const adjustedModelIndex = this.adjustModelLineNumber(modelLineIndex)

      const line = this.modelLineProjections[modelLineIndex]
      if (line.isVisible()) {
        // merge into previous request
        if (reqStart === null) {
          reqStart = new Position(
            adjustedModelIndex + 1,
            modelLineIndex === modelStartLineIndex ? modelStart.column : 1
          )
        }
      } else {
        // hit invisible line => flush request
        if (reqStart !== null) {
          const maxLineColumn = this.model.getLineMaxColumn(adjustedModelIndex)
          result = result.concat(
            this.model.getDecorationsInRange(
              new Range(
                reqStart.lineNumber,
                reqStart.column,
                adjustedModelIndex,
                maxLineColumn
              ),
              ownerId,
              filterOutValidation,
              onlyMinimapDecorations
            )
          )
          reqStart = null
        }
      }
    }

    if (reqStart !== null) {
      result = result.concat(
        this.model.getDecorationsInRange(
          new Range(
            reqStart.lineNumber,
            reqStart.column,
            this.adjustModelLineNumber(modelEnd.lineNumber),
            modelEnd.column
          ),
          ownerId,
          filterOutValidation,
          onlyMinimapDecorations
        )
      )
      reqStart = null
    }

    result.sort((a, b) => {
      const res = Range.compareRangesUsingStarts(a.range, b.range)
      if (res === 0) {
        if (a.id < b.id) {
          return -1
        }
        if (a.id > b.id) {
          return 1
        }
        return 0
      }
      return res
    })

    // Eliminate duplicate decorations that might have intersected our visible ranges multiple times
    const finalResult: IModelDecoration[] = []
    let finalResultLen = 0
    let prevDecId: string | null = null
    for (const dec of result) {
      const decId = dec.id
      if (prevDecId === decId) {
        // skip
        continue
      }
      prevDecId = decId
      finalResult[finalResultLen++] = dec
    }

    return finalResult
  }

  public getInjectedTextAt(position: Position): InjectedText | null {
    const info = this.getViewLineInfo(position.lineNumber)
    return this.modelLineProjections[
      info.modelLineNumber - 1
    ].getInjectedTextAt(info.modelLineWrappedLineIdx, position.column)
  }

  normalizePosition(position: Position, affinity: PositionAffinity): Position {
    const info = this.getViewLineInfo(position.lineNumber)
    return this.modelLineProjections[
      info.modelLineNumber - 1
    ].normalizePosition(info.modelLineWrappedLineIdx, position, affinity)
  }

  public getLineIndentColumn(lineNumber: number): number {
    const info = this.getViewLineInfo(lineNumber)
    if (info.modelLineWrappedLineIdx === 0) {
      return this.model.getLineIndentColumn(info.modelLineNumber)
    }

    // wrapped lines have no indentation.
    // We deliberately don't handle the case that indentation is wrapped
    // to avoid two view lines reporting indentation for the very same model line.
    return 0
  }
}

/**
 * Overlapping unsorted ranges:
 * [   )      [ )       [  )
 *    [    )      [       )
 * ->
 * Non overlapping sorted ranges:
 * [       )  [ ) [        )
 *
 * Note: This function only considers line information! Columns are ignored.
 */
function normalizeLineRanges(ranges: Range[]): Range[] {
  if (ranges.length === 0) {
    return []
  }

  const sortedRanges = ranges.slice()
  sortedRanges.sort(Range.compareRangesUsingStarts)

  const result: Range[] = []
  let currentRangeStart = sortedRanges[0].startLineNumber
  let currentRangeEnd = sortedRanges[0].endLineNumber

  for (let i = 1, len = sortedRanges.length; i < len; i++) {
    const range = sortedRanges[i]

    if (range.startLineNumber > currentRangeEnd + 1) {
      result.push(new Range(currentRangeStart, 1, currentRangeEnd, 1))
      currentRangeStart = range.startLineNumber
      currentRangeEnd = range.endLineNumber
    } else if (range.endLineNumber > currentRangeEnd) {
      currentRangeEnd = range.endLineNumber
    }
  }
  result.push(new Range(currentRangeStart, 1, currentRangeEnd, 1))
  return result
}

/**
 * Represents a view line. Can be used to efficiently query more information about it.
 */
class ViewLineInfo {
  public get isWrappedLineContinuation(): boolean {
    return this.modelLineWrappedLineIdx > 0
  }

  constructor(
    public readonly modelLineNumber: number,
    public readonly modelLineWrappedLineIdx: number
  ) {}
}

/**
 * A list of view lines that have a contiguous span in the model.
 */
class ViewLineInfoGroupedByModelRange {
  constructor(
    public readonly modelRange: Range,
    public readonly viewLines: ViewLineInfo[]
  ) {}
}

class CoordinatesConverter implements ICoordinatesConverter {
  private readonly _lines: ViewModelLinesFromProjectedModel

  constructor(
    lines: ViewModelLinesFromProjectedModel,
    private readonly _editRange: IRange | undefined
  ) {
    this._lines = lines
  }

  // View -> Model conversion and related methods

  public convertViewPositionToModelPosition(viewPosition: Position): Position {
    return this._lines.convertViewPositionToModelPosition(
      viewPosition.lineNumber,
      viewPosition.column
    )
  }

  public convertViewRangeToModelRange(viewRange: Range): Range {
    return this._lines.convertViewRangeToModelRange(viewRange)
  }

  public validateViewPosition(
    viewPosition: Position,
    expectedModelPosition: Position
  ): Position {
    return this._lines.validateViewPosition(
      viewPosition.lineNumber,
      viewPosition.column,
      expectedModelPosition
    )
  }

  public validateViewRange(viewRange: Range, expectedModelRange: Range): Range {
    return this._lines.validateViewRange(viewRange, expectedModelRange)
  }

  // Model -> View conversion and related methods

  public convertModelPositionToViewPosition(
    modelPosition: Position,
    affinity?: PositionAffinity,
    allowZero?: boolean,
    belowHiddenRanges?: boolean
  ): Position {
    return this._lines.convertModelPositionToViewPosition(
      modelPosition.lineNumber,
      modelPosition.column,
      affinity,
      allowZero,
      belowHiddenRanges
    )
  }

  public convertModelRangeToViewRange(
    modelRange: Range,
    affinity?: PositionAffinity
  ): Range {
    return this._lines.convertModelRangeToViewRange(modelRange, affinity)
  }

  public modelPositionIsVisible(modelPosition: Position): boolean {
    const adjustedPosition = this._editRange
      ? modelPosition.delta(-this._editRange.startLineNumber + 1)
      : modelPosition
    return this._lines.modelPositionIsVisible(
      adjustedPosition.lineNumber,
      adjustedPosition.column
    )
  }

  public getModelLineViewLineCount(modelLineNumber: number): number {
    return this._lines.getModelLineViewLineCount(modelLineNumber)
  }

  public getViewLineNumberOfModelPosition(
    modelLineNumber: number,
    modelColumn: number
  ): number {
    return this._lines.getViewLineNumberOfModelPosition(
      modelLineNumber,
      modelColumn
    )
  }
}

const enum IndentGuideRepeatOption {
  BlockNone = 0,
  BlockSubsequent = 1,
  BlockAll = 2,
}

export class ViewModelLinesFromModelAsIs implements IViewModelLines {
  public readonly model: ITextModel

  constructor(
    model: ITextModel,
    private readonly _editRange: IRange | undefined
  ) {
    this.model = model
  }

  public dispose(): void {}

  public createCoordinatesConverter(): ICoordinatesConverter {
    return new IdentityCoordinatesConverter(this, this._editRange)
  }

  public getHiddenAreas(): Range[] {
    return []
  }

  public setHiddenAreas(_ranges: Range[]): boolean {
    return false
  }

  public setTabSize(_newTabSize: number): boolean {
    return false
  }

  public setWrappingSettings(
    _fontInfo: FontInfo,
    _wrappingStrategy: "simple" | "advanced",
    _wrappingColumn: number,
    _wrappingIndent: WrappingIndent
  ): boolean {
    return false
  }

  public createLineBreaksComputer(): ILineBreaksComputer {
    const result: null[] = []
    return {
      addRequest: (
        lineText: string,
        injectedText: LineInjectedText[] | null,
        previousLineBreakData: ModelLineProjectionData | null
      ) => {
        result.push(null)
      },
      finalize: () => {
        return result
      },
    }
  }

  public onModelFlushed(): void {}

  public onModelLinesDeleted(
    _versionId: number | null,
    fromLineNumber: number,
    toLineNumber: number
  ): viewEvents.ViewLinesDeletedEvent | null {
    return new viewEvents.ViewLinesDeletedEvent(fromLineNumber, toLineNumber)
  }

  public onModelLinesInserted(
    _versionId: number | null,
    fromLineNumber: number,
    toLineNumber: number,
    lineBreaks: (ModelLineProjectionData | null)[]
  ): viewEvents.ViewLinesInsertedEvent | null {
    return new viewEvents.ViewLinesInsertedEvent(fromLineNumber, toLineNumber)
  }

  public onModelLineChanged(
    _versionId: number | null,
    lineNumber: number,
    lineBreakData: ModelLineProjectionData | null
  ): [
    boolean,
    viewEvents.ViewLinesChangedEvent | null,
    viewEvents.ViewLinesInsertedEvent | null,
    viewEvents.ViewLinesDeletedEvent | null
  ] {
    return [
      false,
      new viewEvents.ViewLinesChangedEvent(lineNumber, 1),
      null,
      null,
    ]
  }

  public acceptVersionId(_versionId: number): void {}

  public getViewLineCount(): number {
    return this._editRange
      ? this._editRange.endLineNumber
      : this.model.getLineCount()
  }

  public getActiveIndentGuide(
    viewLineNumber: number,
    _minLineNumber: number,
    _maxLineNumber: number
  ): IActiveIndentGuideInfo {
    return {
      startLineNumber: viewLineNumber,
      endLineNumber: viewLineNumber,
      indent: 0,
    }
  }

  public getViewLinesBracketGuides(
    startLineNumber: number,
    endLineNumber: number,
    activePosition: IPosition | null
  ): IndentGuide[][] {
    return new Array(endLineNumber - startLineNumber + 1).fill([])
  }

  public getViewLinesIndentGuides(
    viewStartLineNumber: number,
    viewEndLineNumber: number
  ): number[] {
    const viewLineCount = viewEndLineNumber - viewStartLineNumber + 1
    const result = new Array<number>(viewLineCount)
    for (let i = 0; i < viewLineCount; i++) {
      result[i] = 0
    }
    return result
  }

  public getViewLineContent(viewLineNumber: number): string {
    return this.model.getLineContent(viewLineNumber)
  }

  public getViewLineLength(viewLineNumber: number): number {
    return this.model.getLineLength(viewLineNumber)
  }

  public getViewLineMinColumn(viewLineNumber: number): number {
    return this.model.getLineMinColumn(viewLineNumber)
  }

  public getViewLineMaxColumn(viewLineNumber: number): number {
    return this.model.getLineMaxColumn(viewLineNumber)
  }

  public getViewLineData(viewLineNumber: number): ViewLineData {
    const lineTokens = this.model.tokenization.getLineTokens(viewLineNumber)
    const lineContent = lineTokens.getLineContent()
    return new ViewLineData(
      lineContent,
      false,
      1,
      lineContent.length + 1,
      0,
      lineTokens.inflate(),
      null
    )
  }

  public getViewLinesData(
    viewStartLineNumber: number,
    viewEndLineNumber: number,
    needed: boolean[]
  ): Array<ViewLineData | null> {
    const lineCount = this.getViewLineCount()
    viewStartLineNumber = Math.min(Math.max(1, viewStartLineNumber), lineCount)
    viewEndLineNumber = Math.min(Math.max(1, viewEndLineNumber), lineCount)

    const result: Array<ViewLineData | null> = []
    for (
      let lineNumber = viewStartLineNumber;
      lineNumber <= viewEndLineNumber;
      lineNumber++
    ) {
      const idx = lineNumber - viewStartLineNumber
      result[idx] = needed[idx] ? this.getViewLineData(lineNumber) : null
    }

    return result
  }

  public getDecorationsInRange(
    range: Range,
    ownerId: number,
    filterOutValidation: boolean,
    onlyMinimapDecorations: boolean,
    onlyMarginDecorations: boolean
  ): IModelDecoration[] {
    return this.model.getDecorationsInRange(
      range,
      ownerId,
      filterOutValidation,
      onlyMinimapDecorations,
      onlyMarginDecorations
    )
  }

  normalizePosition(position: Position, affinity: PositionAffinity): Position {
    return this.model.normalizePosition(position, affinity)
  }

  public getLineIndentColumn(lineNumber: number): number {
    return this.model.getLineIndentColumn(lineNumber)
  }

  public getInjectedTextAt(position: Position): InjectedText | null {
    // Identity lines collection does not support injected text.
    return null
  }
}

class IdentityCoordinatesConverter implements ICoordinatesConverter {
  private readonly _lines: ViewModelLinesFromModelAsIs

  constructor(
    lines: ViewModelLinesFromModelAsIs,
    private readonly _editRange: IRange | undefined
  ) {
    this._lines = lines
  }

  private _validPosition(pos: Position): Position {
    return this._lines.model.validatePosition(pos)
  }

  private _validRange(range: Range): Range {
    return this._lines.model.validateRange(range)
  }

  // View -> Model conversion and related methods

  public convertViewPositionToModelPosition(viewPosition: Position): Position {
    return this._validPosition(viewPosition)
  }

  public convertViewRangeToModelRange(viewRange: Range): Range {
    return this._validRange(viewRange)
  }

  public validateViewPosition(
    _viewPosition: Position,
    expectedModelPosition: Position
  ): Position {
    return this._validPosition(expectedModelPosition)
  }

  public validateViewRange(
    _viewRange: Range,
    expectedModelRange: Range
  ): Range {
    return this._validRange(expectedModelRange)
  }

  // Model -> View conversion and related methods

  public convertModelPositionToViewPosition(modelPosition: Position): Position {
    return this._validPosition(modelPosition)
  }

  public convertModelRangeToViewRange(modelRange: Range): Range {
    return this._validRange(modelRange)
  }

  public modelPositionIsVisible(modelPosition: Position): boolean {
    const lineCount = this._lines.model.getLineCount()
    if (
      modelPosition.lineNumber < 1 ||
      modelPosition.lineNumber > (this._editRange?.endLineNumber ?? lineCount)
    ) {
      // invalid arguments
      return false
    }
    return true
  }

  public modelRangeIsVisible(modelRange: Range): boolean {
    const lineCount = this._lines.model.getLineCount()
    if (
      modelRange.startLineNumber < 1 ||
      modelRange.startLineNumber > lineCount
    ) {
      // invalid arguments
      return false
    }
    if (modelRange.endLineNumber < 1 || modelRange.endLineNumber > lineCount) {
      // invalid arguments
      return false
    }
    return true
  }

  public getModelLineViewLineCount(modelLineNumber: number): number {
    return 1
  }

  public getViewLineNumberOfModelPosition(
    modelLineNumber: number,
    modelColumn: number
  ): number {
    return modelLineNumber
  }
}
